using System;
using System.Linq;
using System.Windows;
using System.Windows.Input.Manipulations;
using System.Windows.Interactivity;
using System.Windows.Media;

namespace Wintellect.Touch
{
    public abstract class BaseResizingBehavior<T> : Behavior<T> where T: FrameworkElement 
    {
        #region Fields
        private Manipulate2DBehavior _manipulationBehavior;
        private FrameworkElement _associatedObjectParent;
        #endregion

        #region Behavior Overrides

        /// <summary>
        /// Called after the behavior is attached to an AssociatedObject.
        /// </summary>
        /// <remarks>Override this to hook up functionality to the AssociatedObject.</remarks>
        protected override void OnAttached()
        {
            base.OnAttached();

            if (!(AssociatedObject.RenderTransform is CompositeTransform))
            {
                AssociatedObject.RenderTransform = new CompositeTransform();
                //AssociatedObject.RenderTransformOrigin = new Point(0.5, 0.5);
            }

            // Find an associated manipulation behavior if one exists.  If not, create one and attach it.
            var behaviors = Interaction.GetBehaviors(AssociatedObject);
            _manipulationBehavior = behaviors.FirstOrDefault(x => x is Manipulate2DBehavior) as Manipulate2DBehavior;
            if (_manipulationBehavior == null)
            {
                _manipulationBehavior = new Manipulate2DBehavior { IsInertial = true, InertiaDeceleration = (15.0 * 96.0f) / (1000.0f * 1000.0f) };

                // Defer the attach operation so it doesn't happen within this Attach, or an exception is raised.
                AssociatedObject.Dispatcher.BeginInvoke(() => behaviors.Add(_manipulationBehavior));
            }

            // Subscribe to the manipulation events
            _manipulationBehavior.ManipulationStarted += OnManipulationStarted;
            _manipulationBehavior.ManipulationDelta += OnManipulationDelta;
            _manipulationBehavior.ManipulationCompleted += OnManipulationCompleted;

            RoutedEventHandler loadedHandler = null;
            loadedHandler = (o, e) =>
            {
                // Only handle loaded once, then detach the listener.
                AssociatedObject.Loaded -= loadedHandler;

                // Cache off a reference to the associated object's parent for later re-use
                _associatedObjectParent = AssociatedObject.Parent as FrameworkElement;

                // Listen to the parent's resize event - if clipping to parent is engaged, a resize will need to cause changes 
                if (_associatedObjectParent != null) _associatedObjectParent.SizeChanged += HandleAssociatedObjectParentSizeChanged;

                // 'Prime the pump' for parent clipping
                OnClipToParentChanged();
            };

            AssociatedObject.Loaded += loadedHandler;
        }

        /// <summary>
        /// Called when the behavior is being detached from its AssociatedObject, but before it has actually occurred.
        /// </summary>
        /// <remarks>Override this to unhook functionality from the AssociatedObject.</remarks>
        protected override void OnDetaching()
        {
            base.OnDetaching();

            // Unsubscribe from the manipulation events
            _manipulationBehavior.ManipulationStarted -= OnManipulationStarted;
            _manipulationBehavior.ManipulationDelta -= OnManipulationDelta;
            _manipulationBehavior.ManipulationCompleted -= OnManipulationCompleted;

            // Unsubscribe from parent resize events
            if (_associatedObjectParent != null) _associatedObjectParent.SizeChanged -= HandleAssociatedObjectParentSizeChanged;

            // Release the behavior
            _manipulationBehavior = null;
        }

        #endregion

        #region Manipulation Event Handlers

        /// <summary>
        /// Called when a manipulation is started on the <see cref="Behavior.AssociatedObject"/>.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.Manipulations.Manipulation2DStartedEventArgs"/> instance containing the event data.</param>
        private void OnManipulationStarted(Object sender, Manipulation2DStartedEventArgs e)
        {
        }

        /// <summary>
        /// Called when a manipulation delta occurs on the <see cref="Behavior.AssociatedObject"/>.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.Manipulations.Manipulation2DDeltaEventArgs"/> instance containing the event data.</param>
        private void OnManipulationDelta(Object sender, Manipulation2DDeltaEventArgs e)
        {
            var transform = (CompositeTransform)AssociatedObject.RenderTransform;

            var desiredXScale = transform.ScaleX * e.Delta.ScaleX;
            var desiredYScale = transform.ScaleY * e.Delta.ScaleY;
            var normalizedScales = GetNormalizedScales(desiredXScale, desiredYScale);
            transform.ScaleX = normalizedScales.X;
            transform.ScaleY = normalizedScales.Y;

            var desiredXTranslation = transform.TranslateX + e.Delta.TranslationX;
            var desiredYTranslation = transform.TranslateY + e.Delta.TranslationY;
            var normalizedTranslations = GetNormalizedTranslations(transform.ScaleX, transform.ScaleY, desiredXTranslation, desiredYTranslation);
            transform.TranslateX = normalizedTranslations.X;
            transform.TranslateY = normalizedTranslations.Y;
        }

        /// <summary>
        /// Called when a manipulation completed on the <see cref="Behavior{T}.AssociatedObject"/>.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.Manipulations.Manipulation2DCompletedEventArgs"/> instance containing the event data.</param>
        private void OnManipulationCompleted(Object sender, Manipulation2DCompletedEventArgs e)
        {
        }
        #endregion

        #region Other Event Handlers
        private void HandleAssociatedObjectParentSizeChanged(object sender, SizeChangedEventArgs e)
        {
            OnClipToParentChanged();
        }

        #endregion

        #region ClipToParent DP

        /// <summary>
        /// Gets or sets a value indicating whether to clip the resized image to its parent.
        /// </summary>
        /// <value><c>true</c> if [clip to parent]; otherwise, <c>false</c>.</value>
        public Boolean ClipToParent
        {
            get { return (Boolean)GetValue(ClipToParentProperty); }
            set { SetValue(ClipToParentProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ClipToParent.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ClipToParentProperty =
            DependencyProperty.Register("ClipToParent", typeof(Boolean), typeof(BaseResizingBehavior<T>), new PropertyMetadata(true, OnClipToParentChanged));


        private static void OnClipToParentChanged(DependencyObject source, DependencyPropertyChangedEventArgs args)
        {
            var sourceBehavior = (BaseResizingBehavior<T>)source;
            if (sourceBehavior != null)
            {
                sourceBehavior.OnClipToParentChanged();
            }
        }

        private void OnClipToParentChanged()
        {
            if (_associatedObjectParent == null) return;
            _associatedObjectParent.Clip = ClipToParent
                                               ? new RectangleGeometry
                                               {
                                                   Rect = new Rect(0, 0, _associatedObjectParent.ActualWidth, _associatedObjectParent.ActualHeight)
                                               }
                                               : null;
        }
        #endregion

        #region LimitMinimumScaleToOriginalSize DP

        /// <summary>
        /// Gets or sets a value indicating whether to limit the minimum scale to the original size (don't allow &lt; 1.0).
        /// </summary>
        /// <value>
        /// 	<c>true</c> if [limit minimum scale to original size]; otherwise, <c>false</c>.
        /// </value>
        public Boolean LimitMinimumScaleToOriginalSize
        {
            get { return (Boolean)GetValue(LimitMinimumScaleToOriginalSizeProperty); }
            set { SetValue(LimitMinimumScaleToOriginalSizeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for LimitMinimumToOriginalSize.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LimitMinimumScaleToOriginalSizeProperty =
            DependencyProperty.Register("LimitMinimumScaleToOriginalSize", typeof(Boolean), typeof(BaseResizingBehavior<T>), new PropertyMetadata(true, OnLimitMinimumScaleToOriginalSizeChanged));

        private static void OnLimitMinimumScaleToOriginalSizeChanged(DependencyObject source, DependencyPropertyChangedEventArgs args)
        {
            var sourceBehavior = (BaseResizingBehavior<T>)source;
            sourceBehavior.OnLimitMinimumScaleToOriginalSizeChanged();
        }

        private void OnLimitMinimumScaleToOriginalSizeChanged()
        {
            //possibly reset transform size
            if (LimitMinimumScaleToOriginalSize)
            {
                var transform = (CompositeTransform)AssociatedObject.RenderTransform;

                var normalizedScales = GetNormalizedScales(transform.ScaleX, transform.ScaleY);
                transform.ScaleX = normalizedScales.X;
                transform.ScaleY = normalizedScales.Y;

                //it may also be necessary to reset the translation...
                var normalizedTranslations = GetNormalizedTranslations(transform.ScaleX, transform.ScaleY, transform.TranslateX, transform.TranslateY);
                transform.TranslateX = normalizedTranslations.X;
                transform.TranslateY = normalizedTranslations.Y;
            }
        }
        #endregion

        #region LimitTranslationToParentBounds DP

        /// <summary>
        /// Gets or sets a value indicating whether translations should be bound by the containing parent 
        /// (don't let the contents scroll out of the parent).
        /// </summary>
        /// <value>
        /// 	<c>true</c> if [limit translation to parent bounds]; otherwise, <c>false</c>.
        /// </value>
        public Boolean LimitTranslationToParentBounds
        {
            get { return (Boolean)GetValue(LimitTranslationToParentBoundsProperty); }
            set { SetValue(LimitTranslationToParentBoundsProperty, value); }
        }

        // Using a DependencyProperty as the backing store for LimitTranslationToParentBounds.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LimitTranslationToParentBoundsProperty =
            DependencyProperty.Register("LimitTranslationToParentBounds", typeof(Boolean), typeof(BaseResizingBehavior<T>), new PropertyMetadata(true, OnLimitTranslationToParentBoundsChanged));

        private static void OnLimitTranslationToParentBoundsChanged(DependencyObject source, DependencyPropertyChangedEventArgs args)
        {
            var sourceBehavior = (BaseResizingBehavior<T>)source;
            sourceBehavior.OnLimitTranslationToParentBoundsChanged();
        }

        private void OnLimitTranslationToParentBoundsChanged()
        {
            if (LimitTranslationToParentBounds)
            {
                var transform = (CompositeTransform)AssociatedObject.RenderTransform;
                var normalizedTranslations = GetNormalizedTranslations(transform.ScaleX, transform.ScaleY, transform.TranslateX, transform.TranslateY);
                transform.TranslateX = normalizedTranslations.X;
                transform.TranslateY = normalizedTranslations.Y;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Immediately resets the scale and translation values.
        /// </summary>
        public void ResetElement()
        {
            ResetScaleAndTranslation();
        }

        #endregion

        #region Helpers

        private Point GetNormalizedScales(Double desiredXScale, Double desiredYScale)
        {
            var result = new Point(desiredXScale, desiredYScale);
            if (LimitMinimumScaleToOriginalSize)
            {
                result.X = Math.Max(desiredXScale, 1.0);
                result.Y = Math.Max(desiredYScale, 1.0);
            }
            return result;
        }

        private Point GetNormalizedTranslations(Double xScale, Double yScale, Double desiredXTranslation, Double desiredYTranslation)
        {
            var result = new Point(desiredXTranslation, desiredYTranslation);
            // Don't allow image right edge to be inside the right margin, don't allow left edge to be GT 0
            // Don't allow image bottom edge to be inside the bottom margin, don't allow top edge to be GT 0
            // Also, Correct after zooming (especially zoom out)
            var scaledWidth = AssociatedObject.ActualWidth * xScale;
            var lowestXShift = -1.0 * (scaledWidth - AssociatedObject.ActualWidth);

            var scaledHeight = AssociatedObject.ActualHeight * yScale;
            var lowestYShift = -1.0 * (scaledHeight - AssociatedObject.ActualHeight);
            if (LimitTranslationToParentBounds)
            {
                result.X = Math.Max(Math.Min(desiredXTranslation, 0), lowestXShift);
                result.Y = Math.Max(Math.Min(desiredYTranslation, 0), lowestYShift);
            }

            return result;
        }

        protected void ResetScaleAndTranslation()
        {
            var transform = (CompositeTransform)AssociatedObject.RenderTransform;
            transform.ScaleX = 1.0;
            transform.ScaleY = 1.0;
            transform.TranslateX = 0.0;
            transform.TranslateY = 0.0;
        }

        #endregion
    }
}